iT邦幫忙

2022 iThome 鐵人賽

DAY 25
2
自我挑戰組

PixelBit 可以這樣玩!系列 第 25

(Day 25)PixelBit 俄羅斯方塊 Tetris(Part 2)

  • 分享至 

  • xImage
  •  

今天將會接續昨天的 PixelBit 俄羅斯方塊 Tetris(Part 1)程式解說,除了可以了解整個遊戲的運作方式、邏輯,在之後想要改遊戲規則、參數,甚至進階做成連線版本都會比較容易。

Yes

Arduino Setup 初始化

  • 設定 UART Baudrate
  • 註冊收到 328P 資料時的事件內容
  • 初始化 TFT,設定方向
  • 初始化 SPIFFS(目前沒有用到
  • 顯示遊戲開機畫面 3 秒
  • 繪製遊戲邊框
  • 繪製積木顏色
  • 啟動遊戲
/* #region  Arduino Setup */
void setup(void)
{
    Serial.begin(UART_BAUDRATE);

    /* #region  註冊按鍵事件 */
    uart.on(ATM_EVN_BTN_A_PRE, '\0', [](const char *temp) {
        btn_a_state = ATM_BTN_PRE;
    });

    uart.on(ATM_EVN_BTN_B_PRE, '\0', [](const char *temp) {
        btn_b_state = ATM_BTN_PRE;
    });

    uart.on(ATM_EVN_BTN_A_REL, '\0', [](const char *temp) {
        btn_a_state = ATM_BTN_REL;
    });

    uart.on(ATM_EVN_BTN_B_REL, '\0', [](const char *temp) {
        btn_b_state = ATM_BTN_REL;
    });
    /* #endregion */

    /* #region  初始化 TFT */
    tft.init();
    tft.setRotation(3);
    tft.setSwapBytes(true);

    /* #endregion */
    /* #region  初始化 SPIFFS */
    if (!SPIFFS.begin()) {
        tft.fillScreen(TFT_BLACK);
        tft.setTextColor(TFT_RED);
        tft.drawString(String("SPIFFS FAILED"), 30, 55, 4);
        while (1)
            yield();
    }
    /* #endregion */

    /* #region  設定 TJpgDec 比例、解碼 Callback*/
    TJpgDec.setJpgScale(1);
    TJpgDec.setCallback(onTJpgDecoded);
    /* #endregion */

    /* #region  顯示遊戲開機畫面 */
    tft.pushImage(52, 0, 135, 240, tet);
    delay(3000);
    /* #endregion */

    /* #region TODO: 顯示遊戲左右 */
    // tft.fillScreen(TFT_BLACK);
    // TJpgDec.drawFsJpg(0, 0, "/tetris.jpg");
    // delay(3000);
    /* #endregion */

    /* #region  繪製遊戲邊框 */
    tft.fillScreen(TFT_BLACK);
    tft.drawLine(35, 19, 201, 19, GREY);
    tft.drawLine(35, 19, 35, 240, GREY);
    tft.drawLine(201, 19, 201, 240, GREY);
    /* #endregion */

    /* #region  繪製積木顏色 */
    make_block(0, TFT_BLACK);     // Type No, Color
    make_block(1, 0x00F0);        // DDDD     RED
    make_block(2, 0xFBE4);        // DD,DD    PUPLE
    make_block(3, 0xFF00);        // D__,DDD  BLUE
    make_block(4, 0xFF87);        // DD_,_DD  GREEN
    make_block(5, 0x87FF);        // __D,DDD  YELLO
    make_block(6, 0xF00F);        // _DD,DD_  LIGHT GREEN
    make_block(7, 0xF8FC);        // _D_,DDD  PINK
    /* #endregion */
    initGame();
}
/* #endregion */

Arduino loop 運行遊戲

  • 檢查 uart 資料
  • 檢查按鍵狀態開始遊戲
  • 檢查遊戲是否結束
  • 設定下一個積木的位置與方向
  • 更新 TFT 螢幕畫面
/* #region  Arduino Loop */
void loop()
{
    static uint32_t update_timer = 0;
    // polling ATmega328P even
    uart.loop();

    if (gameover && btn_b_state == ATM_BTN_PRE) {
        initGame();
        return;
    }

    if (!gameover) {
        if (millis() > update_timer) {
            Point_t next_pos;
            int     next_rot = rot;
            GetNextPosRot(&next_pos, &next_rot);
            update_timer = millis() + 20;
            ReviseScreen(next_pos, next_rot);
        }
    }
}
/* #endregion */

取得下一個積木的位置、旋轉方向

  • 按下 A 鍵,積木將為往左一格
  • 按下 B 鍵,積木將為往右一格
  • 按下 AB 鍵,積木將會往左旋轉 90 度
/* #region  取得下一個積木位置、旋轉方向 */
void GetNextPosRot(Point_t *pnext_pos, int *pnext_rot)
{
    static uint32_t timer = 0;

    KeyPadLoop();

    if (btn_LEFT)
        // 遊戲開始
        started = true;
    if (!started)
        // 遊戲已結束
        return;

    pnext_pos->X = pos.X;
    pnext_pos->Y = pos.Y;

    if (millis() > timer) {
        timer = millis() + game_speed;
        pnext_pos->Y += 1;
    }

    if (btn_LEFT) {
        btn_LEFT = false;
        // 往左一格
        pnext_pos->X -= 1;
    } else if (btn_RIGHT) {
        btn_RIGHT = false;
        // 往右一格
        pnext_pos->X += 1;
    } else if (btn_AB) {
        btn_AB = false;
        // 往左旋轉
        *pnext_rot = (*pnext_rot + block.numRotate - 1) % block.numRotate;
    }
}

根據按鍵狀態設定 Flag,後續判斷動作將會需要

/* #region  根據按鍵狀態設定動作 flag */
bool KeyPadLoop()
{
    // 按 A 放 B
    if (btn_b_state == ATM_BTN_REL && btn_a_state == ATM_BTN_PRE) {
        if (pom == 0) {
            pom = 1;
            ClearKeys();
            btn_LEFT = true;
            return true;
        }
    } else {
        pom = 0;
    }
    // 按 B 放 A
    if (btn_a_state == ATM_BTN_REL && btn_b_state == ATM_BTN_PRE) {
        if (pom2 == 0) {
            pom2 = 1;
            ClearKeys();
            btn_RIGHT = true;
            return true;
        }
    } else {
        pom2 = 0;
    }
    // 按 A、B
    if (btn_a_state == ATM_BTN_PRE && btn_b_state == ATM_BTN_PRE) {
        if (pom3 == 0) {
            pom3 = 1;
            ClearKeys();
            btn_AB = true;
            return true;
        }
    } else {
        pom3 = 0;
    }

    return false;
}
/* #endregion */

修改 Sreen 內容

這裡是遊戲的主要運作規則,他將會檢查積木是否重疊、積木是否已經到底部、刪除消掉的行等等。

/* #region  修改 Screen */
void ReviseScreen(Point_t next_pos, int next_rot)
{
    if (!started)
        return;
    Point_t next_squares[4];
    // 清除積木四個區塊顏色
    for (int i = 0; i < 4; ++i)
        screen[pos.X + block.square[rot][i].X][pos.Y + block.square[rot][i].Y] = 0;

    if (GetSquares(block, next_pos, next_rot, next_squares)) {
        // 無重疊或超出邊界
        for (int i = 0; i < 4; ++i) {
            screen[next_squares[i].X][next_squares[i].Y] = block.color;
        }
        pos = next_pos;
        rot = next_rot;
    } else {
        // 重疊或超出邊界
        // 回填積木四個區塊顏色
        for (int i = 0; i < 4; ++i)
            screen[pos.X + block.square[rot][i].X][pos.Y + block.square[rot][i].Y] = block.color;
        // 檢查積木 Y 座標是否到底
        if (next_pos.Y == pos.Y + 1) {
            ChkDeleteLine();
            PutStartPos();
            if (!GetSquares(block, pos, rot, next_squares)) {
                // 設定新積木
                for (int i = 0; i < 4; ++i)
                    screen[pos.X + block.square[rot][i].X][pos.Y + block.square[rot][i].Y] = block.color;
                // 積木已重疊,遊戲結束
                GameOver();
            }
        }
    }
    Draw();
}
/* #endregion */

這個方法將會檢查積木是否重疊或超出邊界,重疊將會回傳 false

/* #region  檢查是否重疊或超出邊界 */
bool GetSquares(Block_t block, Point_t pos, int rot, Point_t *squares)
{
    bool overlap = false;
    for (int i = 0; i < 4; ++i) {
        Point_t p;
        p.X = pos.X + block.square[rot][i].X;
        p.Y = pos.Y + block.square[rot][i].Y;
        overlap |= p.X < 0 || p.X >= Width || p.Y < 0 || p.Y >= Height || screen[p.X][p.Y] != 0;
        squares[i] = p;
    }
    return !overlap;
}
/* #endregion */

這個方法將會檢查是否有消掉行數,並刪除該行

/* #region  檢查並消除整行 */
void ChkDeleteLine()
{
    // 尋訪 screen row
    for (int j = 0; j < Height; ++j) {
        bool Delete = true;
        //
        // 尋訪 screen col,檢整行是否都有積木
        for (int i = 0; i < Width; ++i)
            if (screen[i][j] == 0)
                Delete = false;
        if (Delete) {
            // 增加分數
            score++;
            // 難度升級,下降速度加快
            if (score % UpgradeThreshold == 0) {
                lvl++;
                game_speed = game_speed - SpeedReduction;
                tft.drawString("LVL:" + String(lvl), 167, 8, 1);
            }
            tft.drawString("SCORE:" + String(score), 38, 8, 1);
            // 從下到上更新積木
            for (int k = j; k >= 1; --k) {
                for (int i = 0; i < Width; ++i) {
                    screen[i][k] = screen[i][k - 1];
                }
            }
        }
    }
}
/* #endregion */

最後是 GameOver 方法,遊戲結束時將會呼叫它,它將會把所有積木設為相同顏色以表示遊戲結束。

/* #region  遊戲結束、將所有積木設為統一顏色 */
void GameOver()
{
    // 將所有區塊
    for (int i = 0; i < Width; ++i)
        for (int j = 0; j < Height; ++j)
            if (screen[i][j] != 0)
                screen[i][j] = 4;
    gameover = true;
}
/* #endregion */

小結

經過以上說明,我們已經將整個遊戲程式流程與邏輯都看過一遍了,接著我們將會根據這些資訊嘗試增加可以互相連線競賽的功能,讓這個遊戲能更有趣更好玩,我們明天見。

更多有趣系列教學文章


上一篇
(Day 24)PixelBit 俄羅斯方塊 Tetris(Part 1)
下一篇
(Day 26)Azure Cutom Vision 註冊帳號與專案準備
系列文
PixelBit 可以這樣玩!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言